package com.zty.redis.util;

import com.alibaba.fastjson.JSON;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * @Author ZTY
 * @Description 建立二级缓存
 *
 * Guava是一个Java开发库，提供了许多实用的工具类和函数，其中包括缓存实现。
 * 它是一个本地缓存，将数据存储在应用程序的内存中。
 * 另一方面，Redis是一个独立的分布式缓存系统，将数据存储在网络中的Redis服务器中。
 *
 * 现在，让我们看看为什么在某些情况下选择Guava二级缓存可能更适合：
 * 速度和性能：Guava二级缓存存储在应用程序的内存中，因此访问速度非常快。
 * 这是因为内存访问比网络访问更快，没有网络延迟和通信开销。
 * 相比之下，Redis缓存需要通过网络连接到Redis服务器，并从服务器中检索数据，这可能会引入一定的延迟。
 *
 * 简单性和易用性：Guava缓存是一个简单而易于使用的本地缓存解决方案。
 * 你只需在应用程序中引入Guava库，并使用Guava提供的缓存接口和方法即可。
 * 相比之下，Redis是一个独立的缓存系统，需要设置和配置Redis服务器，并使用Redis客户端库进行与服务器的通信。
 *
 * 数据一致性：Guava二级缓存存储在应用程序的内存中，这意味着数据的读取和写入是原子操作，保证了数据的一致性。
 * 相比之下，Redis是一个独立的缓存服务器，可能存在网络通信延迟和数据同步延迟，这可能导致数据的不一致性。
 *
 * 适用场景：Guava二级缓存适用于单个应用程序内部的缓存需求，特别适合于需要快速访问和频繁读写的数据。
 * 相比之下，Redis缓存适用于分布式环境下的缓存需求，可以支持多个应用程序之间的数据共享和协同工作。
 *
 * 需要注意的是，Guava二级缓存适用于较小规模的应用程序和数据集，因为它将数据存储在应用程序的内存中。
 * 如果你的应用程序需要缓存大量数据或需要在多个应用程序之间共享缓存数据，那么使用Redis缓存可能更合适。
 *
 * 综上所述，Guava二级缓存是一个简单、快速且易于使用的本地缓存解决方案，适用于单个应用程序内部的缓存需求。
 * 它提供了更快的访问速度和更好的数据一致性。
 * 而Redis缓存则适用于分布式环境下的缓存需求，可以支持多个应用程序之间的数据共享和协同工作。
 * 根据你的具体需求和应用场景，选择适合的缓存解决方案非常重要。
 * @Date 12:23 2023-12-15
 **/

@Component
@Slf4j
public class CacheUtil<K, V> {

    //  制作一个开关，可在外部通过配置决定是否开启
    @Value("${guava.cache.switch}")
    public boolean switchCache;

    /**
     * 初始化本地缓存
     * 时间的配置只能够硬编码
     */
    private Cache<String, String> localCache = CacheBuilder.newBuilder()
            .maximumSize(5000)
            .expireAfterWrite(3, TimeUnit.SECONDS)
            .build();

    /**
     * 模拟通过id查找商品对象的场景
     *
     * @param skuIdList   电商场景下类似于商品的id，希望它的类型和返回值的key类型一致
     * @param cachePrefix 二级缓存是从redis里面拿，拿过来的时候可能将id与某些字段进行拼接作为新key，cachePrefix就是那个要和id拼接的字段
     * @param clazz       要将value序列化成一个对象
     * @param function    定义入参出参，只要入参和出参满足我这个函数的要求，就能使用我这个函数
     * @return
     */
    public Map<K, V> getResult(List<K> skuIdList,
                               String cachePrefix,
                               Class<V> clazz,
                               Function<List<K>, Map<K, V>> function) {

        //  判空
        if (CollectionUtils.isEmpty(skuIdList)) {
            return Collections.emptyMap();
        }

        //  声明一个Map作为返回值的结果
        Map<K, V> resultMap = new HashMap<>(16);

        //  如果关闭了二级缓存，走function函数
        if (!switchCache) {
            resultMap = function.apply(skuIdList);
            return resultMap;
        }

        //  走二级缓存
        //  定义本地缓存中不存在的列表（可能会有一些id已经出现在缓存里了）
        List<K> noCacheIdList = new ArrayList<>();
        for (K id : skuIdList) {
            //  如果key在二级缓存中已有，那我便可以直接取出内容
            String cacheKey = cachePrefix + "_" + id;
            String content = localCache.getIfPresent(cacheKey);
            //  将内容反射回原生的实体
            if (StringUtils.isNotBlank(content)) {
                //  将value序列化成原生的实体
                V v = JSON.parseObject(content, clazz);
                resultMap.put(id, v);
            } else {
                //  如果二级缓存中不存在，则放到缓存不存在列表中
                noCacheIdList.add(id);
            }
        }

        //  noCacheIdList判空，为空则查询列表中的id都已经存在二级缓存中
        if (CollectionUtils.isEmpty(noCacheIdList)) {
            //  直接返回原生实体Map即可
            return resultMap;
        }

        //  二级缓存中没有，还得调用function函数(查了一次RPC)返回结果
        Map<K, V> noCacheResultMap = function.apply(noCacheIdList);

        //  RPC也没查到，只能直接返回从二级缓存中查出来的列表
        if (noCacheResultMap == null || noCacheResultMap.isEmpty()) {
            return resultMap;
        }
        for (Map.Entry<K, V> entry : noCacheResultMap.entrySet()) {
            K id = entry.getKey();
            V result = entry.getValue();

            //  RPC查到后，不仅要将查到的结果放入要返回的Map中，还要放入二级缓存中
            resultMap.put(id, result);

            String cacheKey = cachePrefix + "_" + id;

            //  将返回的结果序列化成JSON格式的String
            localCache.put(cacheKey, JSON.toJSONString(result));
        }

        return resultMap;
    }
}
